package org.springblade.protocol.tcp.server;

import cn.hutool.core.util.HexUtil;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import org.springblade.cache.DeviceManagerCache;
import org.springblade.entity.Product;
import org.springframework.stereotype.Component;

import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * SocketServer
 * @author lijiamin
 */
@Component
public class SocketServer {

	private static Map<String, Channel> mapChannel = new HashMap<>(20);
	private static EventLoopGroup bossGroup;
	private static EventLoopGroup workerGroup;
	private static ServerBootstrap serverBootstrap;
	private static ChannelFuture channelFuture;
	private static Integer port = 13001;

	static {
		bossGroup = new NioEventLoopGroup(3);
		workerGroup = new NioEventLoopGroup(6);
		serverBootstrap = new ServerBootstrap();
		serverBootstrap.option(ChannelOption.SO_REUSEADDR, true)
			.option(ChannelOption.SO_RCVBUF, 1024 * 128)
			.option(ChannelOption.SO_SNDBUF, 1024 * 128)
			.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
			.childOption(ChannelOption.TCP_NODELAY, true);
		serverBootstrap.group(bossGroup, workerGroup)
			.channel(NioServerSocketChannel.class)
			.option(ChannelOption.SO_BACKLOG, 1024)
			.childOption(ChannelOption.SO_KEEPALIVE, true)
			.childHandler(new ChannelInitializer<SocketChannel>() {
				/**
				 * This method will be called once the {@link Channel} was registered. After the method returns this instance
				 * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
				 * @param socketChannel the {@link Channel} which was registered.
				 * @throws Exception is thrown if an error occurs. In that case it will be handled by
				 *                   {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
				 *                   the {@link Channel}.
				 */
				@Override
				protected void initChannel(SocketChannel socketChannel) throws Exception {
					socketChannel.pipeline().addLast(new IdleStateHandler(0, 60, 0, TimeUnit.SECONDS));
					socketChannel.pipeline().addLast(new SocketServerHandle());
				}

			});
	}

	/**
	 * 关闭连接
	 * @param host
	 * @param port
	 */
	public static void closeConnect(String host, Integer port) {
		Channel channel = mapChannel.get(host + ":" + port);
		if (channel == null) {
			// 同时发送离线
			setOrDelServerChannel(host + ":" + port, null);
			Product product = SocketServerHandle.createFixProduct(host, String.valueOf(port), 1);
			DeviceManagerCache.getDeviceManagerClient().updateProtocolStatus(product);
			return;
		}
		channel.close();
	}

	/**
	 * send msg
	 * @param host
	 * @param msg
	 * @param type 1-string 2hex 3byte
	 */
	public static void sendMsg(String host, Integer port, String msg, Integer type) {
		System.out.println("send msg is : " + host + ":" + port + "   " + msg);
		Channel channel = mapChannel.get(host + ":" + port);
		if (channel == null) {
			return;
		}
		if (!channel.isActive()) {
			channel.close();
			return;
		}
		switch (type) {
			case 1:
				ByteBuf byteBuf = ByteBufUtil.encodeString(channel.alloc(), CharBuffer.wrap(msg), StandardCharsets.UTF_8);
				channel.writeAndFlush(byteBuf);
				break;
			case 2:
				channel.writeAndFlush(Unpooled.wrappedBuffer(HexUtil.decodeHex(msg)));
				break;
		}
	}

	/**
	 * save or del
	 * @param address
	 * @param channel
	 */
	public static void setOrDelServerChannel(String address, Channel channel) {
		if (channel == null) {
			mapChannel.remove(address);
		} else {
			mapChannel.put(address, channel);
		}
	}

	/**
	 * 初始化服务
	 */
	public static void initServer() {
		try {
			channelFuture = serverBootstrap.bind(port)
				.addListener((ChannelFutureListener) future -> {
					if (future.isSuccess()) {
						System.out.println("listen port " + port + " succeed");
					} else {
						System.err.println("listen port  " + port + " error!!!!");
					}
				}).channel().closeFuture().sync();
		} catch (Exception e) {
			e.printStackTrace();
			bossGroup.shutdownGracefully();
			workerGroup.shutdownGracefully();
		}
	}

}
